package com.kamistoat.meimeistore.product.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.kamistoat.meimeistore.product.service.CategoryBrandRelationService;
import com.kamistoat.meimeistore.product.vo.thymeleaf.Catalog2Vo;
import com.kamistoat.meimeistore.product.vo.thymeleaf.Catalog3Vo;
import com.kamistoat.meimeistore.product.dao.CategoryDao;
import com.kamistoat.meimeistore.product.entity.CategoryEntity;
import com.kamistoat.meimeistore.product.service.CategoryService;
import org.redisson.api.RLock;
import org.redisson.api.RReadWriteLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.kamistoat.common.utils.PageUtils;
import com.kamistoat.common.utils.Query;

import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;


@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {

    @Autowired
    CategoryBrandRelationService categoryBrandRelationService;

    // 自动注入redis的template，用于使用redis的库构造类
    @Autowired
    StringRedisTemplate stringRedisTemplate;

    // 自动注入Redisson客户端类
    @Autowired
    RedissonClient redissonClient;

    @Override
    public PageUtils queryPage(Map<String, Object> params) {
        IPage<CategoryEntity> page = this.page(
                new Query<CategoryEntity>().getPage(params),
                new QueryWrapper<CategoryEntity>()
        );

        return new PageUtils(page);
    }

    /**
     * 我们自己编写的方法
     * 查找所有的分类。并按照树形结构组装
     */
    @Override
    public List<CategoryEntity> listWithTree() {
        // 由于类继承了ServiceImpl<CategoryDao, CategoryEntity>，
        // 所以baseMapper就是CategoryDao
        // 当然如果不嫌麻烦，也可以在类头自动注入CategoryDao来使用
        // 查询条件null代表查询所有数据
        // Dao.selectList()可以按照条件进行批量查询，查询结果将会自动封装成List形式
        List<CategoryEntity> categoryEntityList = baseMapper.selectList(null);
        if (categoryEntityList.size() == 0) {
            return Collections.emptyList();
        } else {
            // 组装树形的父子结构
            // 首先找出所有的一级分类————其parent_cid为0，也就是没有父id
            // 因此使用.stream().filter(函数体)对所有分类进行筛选，找到ParentCid==0
            // stream编程中，.filter(()->{})可以看做是一个插件，它的作用就是对.前的list进行过滤
            // .filter()插件的存在不影响.map()的位置，.map()可以在.filter()的前面或后面，但是.filter()总是对它前面的list进行过滤
            List<CategoryEntity> level1Menus = categoryEntityList.stream().filter((categoryEntity) -> {
                // .filter()前是原始的list
                // .filter()的return值是true/false，如果返回true，则保留这一条数据，如果为false则删除这一条数据。
                return categoryEntity.getParentCid() == 0;
            }).map((menu) -> {
                // .map()跟在.filter()之后，此时经过.filter()得到的list中只有一级菜单分类，.map()是在这个一级菜单list上进行操作
                // 对这个list中的每一个一级菜单，继续寻找每个一级分类的子分类
                // 寻找子分类是一个递归方法，它会从当前这个第一级分类开始，一直向下找到每一级分类的子分类
                menu.setChildren(getChildren(menu, categoryEntityList));
                return menu;
            }).sorted((menu1, menu2) -> {
                // 此时经过.map()后得到的是某一级上的全部分类(实体类中的children保存下一级分类)。再将他们按照其自身的Sort值来进行排序
                // 由于在有些数据的sort列没有填数据，也就是null，不加处理会出错
                return (menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort());
            }).collect(Collectors.toList());

            return level1Menus;
        }
    }


    /**
     * 一个递归方法，用于从全体分类中寻找当前分类的子分类
     *
     * @param categoryEntity     当前的某一级分类
     * @param categoryEntityList 全部分类原始list
     * @return 当前某一级分类的子分类
     */
    public List<CategoryEntity> getChildren(CategoryEntity categoryEntity, List<CategoryEntity> categoryEntityList) {
        // 同样使用.stream().filter(函数体)对所有分类进行筛选，找到ParentCid==当前分类的id
        List<CategoryEntity> childrenMenus = categoryEntityList.stream().filter((categoryEntityF) -> {
            return categoryEntityF.getParentCid().equals(categoryEntity.getCatId());
            // 第一次执行到这里，找出了第一级分类的所有子分类
            // 之后递归每次执行到这里，都是在向下寻找子分类
        }).map((menu) -> {
            // 遍历刚刚找到的子分类，然后这里就是执行递归了
            // 将自身作为getChildren的输入变量，再去找自身的子分类
            menu.setChildren(getChildren(menu, categoryEntityList));
            return menu;
        }).sorted((menu1, menu2) -> {
            return (menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort());
        }).collect(Collectors.toList());

        return childrenMenus;
    }

    /**
     * 我们自己编写的方法
     * 在一定的逻辑条件下，对菜单进行删除
     *
     * @param asList 传入的菜单id列表
     */
    @Override
    public void removeMenuByIds(List<Long> asList) {
        // 暂时我们还没有写检查是否允许删除的代码
        // TODO  首先检查当前要删除的菜单，是否被别的地方引用

        // 直接调用baseMapper的deleteBatchIds就可以批量删除
        // 但这是物理删除，真删了，以后也找不回来了。我们建议使用逻辑删除。
        // 只需要开启mybatis的逻辑删除功能，依然调用deleteBatchIds，但就会变成是逻辑删除
        baseMapper.deleteBatchIds(asList);
    }

    /**
     * 我们自己编写的方法
     * 根据传入的菜单id，寻找其全部父菜单id，制作成一个long[]返回
     *
     * @param catelogId 某个底层菜单Id
     * @return long[]全部父菜单Id+自身Id
     */
    @Override
    public Long[] findCatelogPath(Long catelogId) {
        // 查菜单路径
        List<Long> catelogPathList = new ArrayList<Long>();
        CategoryEntity categoryEntity = this.getById(catelogId);
        while (categoryEntity != null) {
            // 只要非空，一定就是还有父菜单，就一直往上找父菜单Id
            catelogPathList.add(categoryEntity.getCatId());
            categoryEntity = this.getById(categoryEntity.getParentCid());
        }
        Collections.reverse(catelogPathList);
        Long[] catelogPath = catelogPathList.toArray(new Long[0]);
        return catelogPath;
    }

    /**
     * 我们自己编写的方法
     * 一并更新所有冗余存储的数据表
     * <p>
     * 使用SpringCache来实现缓存层
     * 方法updateDetailWithRedisson()手动实现reids，且使用Redisson为程序加写锁
     * <p>
     * 使用@CacheEvict注解开启缓存失效模式保证一致性，记得指定缓存空间和键名
     *
     * @param category 传来的Entity
     */
    @Caching(evict = {
            @CacheEvict(value = {"category"}, key = "'TopCategorys'"),
            @CacheEvict(value = "category", key = "'Sec&ThrCategorys'")
    })
    @Override
    @Transactional
    public void updateDetail(CategoryEntity category) {
        // 先把自身数据表给更新了
        this.updateById(category);
        if (!StringUtils.isEmpty(category.getName())) {
            // 调用冗余字段所在的service，对冗余字段进行更新
            categoryBrandRelationService.updateCBRelation_Category(category.getCatId(), category.getName());
        }
        // TODO  如果有其他引用了category字段的地方，还需要更新其他冗余字段
    }

    /**
     * 我们自己编写的方法
     * 一并更新所有冗余存储的数据表
     * <p>
     * 手动实现reids，且使用Redisson为程序加写锁
     *
     * @param category 传来的Entity
     */
    @Transactional
    public void updateDetailWithRedisson(CategoryEntity category) {
        // 获得一个读写锁来锁住修改
        RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("product-CatalogJson-Lock");
        RLock rLock = readWriteLock.writeLock();
        rLock.lock();
        // 加了写锁后才允许对mysql进行修改
        try {
            // 先把自身数据表给更新了
            this.updateById(category);
            if (!StringUtils.isEmpty(category.getName())) {
                // 调用冗余字段所在的service，对冗余字段进行更新
                categoryBrandRelationService.updateCBRelation_Category(category.getCatId(), category.getName());
            }
            // TODO  如果有其他引用了category字段的地方，还需要更新其他冗余字段
        } finally {
            // 无论修改是否成功，都应该删除掉缓存中的数据。注意键名不要写错了
            stringRedisTemplate.delete("catalogJSON");
            // 同时还要释放写锁
            rLock.unlock();
        }
    }

    /**
     * thymeleaf专用
     * 我们自己编写的方法，返回所有的一级分类菜单
     * 使用@Cacheable({"category"})是SpringCache的配置注解。声明该方法返回结果从属于缓存层。缓存数据位于名为category的缓存空间中
     *
     * @return 列表
     */
    @Cacheable(value = {"category"}, key = "'TopCategorys'")
    @Override
    public List<CategoryEntity> getTopCategorys() {
        System.out.println("获取一级菜单");  // 打印语句是用于测试SpringCache的
        List<CategoryEntity> categoryEntityList = this.list(new QueryWrapper<CategoryEntity>().eq("cat_level", 1));
        return categoryEntityList;
    }


    /**
     * thymeleaf专用
     * 从缓存redis中查询二级和三级分类菜单并返回，如果有就直接返回。
     * 如果没有，则调用下面的方法从数据库中查询，返回的同时再将数据在redis中备份
     * 注意规定在redis中使用<String,JSONString>存放数据
     * <p>
     * 使用SpringCache实现在redis中缓存的功能，getCatalogJsonWithRedis()方法是手动实现
     * <p>
     * 使用@Cacheable开启SpringCache，指定缓存空间名和key
     *
     * @return
     */
    @Cacheable(value = "category", key = "'Sec&ThrCategorys'")
    @Override
    public Map<String, List<Catalog2Vo>> getCatalogJson() {
        System.out.println("获取三级菜单");
        // 不再手动去redis中看看有没有现成的缓存数据
        List<CategoryEntity> categoryEntityList = baseMapper.selectList(null);
        Map<String, List<Catalog2Vo>> catalogJson = new HashMap<>();
        List<CategoryEntity> topCategoryList = getByParentId(categoryEntityList, 0L);
        for (CategoryEntity category1Entity : topCategoryList) {
            List<CategoryEntity> catalog2List =
                    getByParentId(categoryEntityList, category1Entity.getCatId());
            if (catalog2List != null && catalog2List.size() > 0) {
                List<Catalog2Vo> catalog2VoList = new ArrayList<>();
                for (CategoryEntity category2Entity : catalog2List) {
                    Catalog2Vo catalog2Vo = new Catalog2Vo();
                    catalog2Vo.setCatalog1Id(category1Entity.getCatId().toString());
                    catalog2Vo.setId(category2Entity.getCatId().toString());
                    catalog2Vo.setName(category2Entity.getName());
                    List<Catalog3Vo> catalog3VoList = new ArrayList<>(Collections.emptyList());
                    List<CategoryEntity> catalog3List =
                            getByParentId(categoryEntityList, category2Entity.getCatId());
                    if (catalog3List != null && catalog3List.size() > 0) {
                        for (CategoryEntity category3Entity : catalog3List) {
                            Catalog3Vo catalog3Vo = new Catalog3Vo();
                            catalog3Vo.setCatalog2Id(category2Entity.getCatId().toString());
                            catalog3Vo.setId(category3Entity.getCatId().toString());
                            catalog3Vo.setName(category3Entity.getName());
                            catalog3VoList.add(catalog3Vo);
                        }
                    }
                    catalog2Vo.setCatalog3List(catalog3VoList);
                    catalog2VoList.add(catalog2Vo);
                }
                catalogJson.put(category1Entity.getCatId().toString(), catalog2VoList);
            }
        }
        // 从数据库中查完之后也不再手动把数据缓存到redis中
        return catalogJson;
    }


    /**
     * thymeleaf专用
     * 从缓存redis中查询二级和三级分类菜单并返回，如果有就直接返回。
     * 如果没有，则调用下面的方法从数据库中查询，返回的同时再将数据在redis中备份
     * 注意规定在redis中使用<String,JSONString>存放数据
     * <p>
     * 手动在实现在redis中缓存的功能
     *
     * @return
     */
    public Map<String, List<Catalog2Vo>> getCatalogJsonWithRedis() {
        /**
         * TODO 缓存穿透/雪崩/击穿
         * 1、空结果缓存，解决缓存穿透问题
         * 2、设置随机过期时间，解决缓存雪崩
         * 3、加锁，解决缓存击穿
         */

        // TODO 大迸发下一定会出现堆外内存异常
        // 1、先从redis中查询数据
        // 用构造器新建一个redis数据操作类
        ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
        // 注意，查询的结果是String，而getCatalogJsonByDB()方法本身返回的是一个Map，因此注意存在数据转换的问题
        // 规定缓存中使用JSON字符串作为值存放
        String catalogJOSN = ops.get("catalogJOSN");
        if (!StringUtils.isEmpty(catalogJOSN)) {
            // 如果缓存中有数据
            // 从redis中拿到的JOSNString要转换成Map
            Map<String, List<Catalog2Vo>> catalogJsonByRedis = JSON.parseObject(catalogJOSN,
                    new TypeReference<Map<String, List<Catalog2Vo>>>() {
                    });
            return catalogJsonByRedis;
        } else {
            // 如果缓存中没有数据
            // 从数据库中查询数据并封装并放到缓存中，注意现在返回的是一个Map
            // Map<String, List<Catalog2Vo>> catalogJsonByDB = getCatalogJsonByDBWithLocalLock();  // 本地锁
            Map<String, List<Catalog2Vo>> catalogJsonByDB = getCatalogJsonByDBWithRedissonLock();  // redis分布式锁
            return catalogJsonByDB;
        }
    }

    /**
     * thymeleaf专用
     * 从数据库中返回所有的二级分类菜单和三级分类菜单，并封装成thymeleaf可以使用的格式
     * 只有当redis中不存在所需要的数据时才会调用这个方法
     * <p>
     * 使用本地锁
     *
     * @return
     */
    public Map<String, List<Catalog2Vo>> getCatalogJsonByDBWithLocalLock() {
        synchronized (this) {
            ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
            // 每次释放锁后进入的线程，先去缓存中查询一次，看看有没有数据
            // 如果有数据，就直接将缓存中的数据返回
            String catalogJOSN = ops.get("catalogJOSN");
            if (!StringUtils.isEmpty(catalogJOSN)) {
                Map<String, List<Catalog2Vo>> catalogJsonByRedis = JSON.parseObject(catalogJOSN,
                        new TypeReference<Map<String, List<Catalog2Vo>>>() {
                        });
                return catalogJsonByRedis;
            }
            // 否则继续执行原来getCatalogJsonByDB()方法体中的代码
            ///////////////// 优化1：先把所有的查出来放在本地，接下来都从本地的这个list中找 ////////
            List<CategoryEntity> categoryEntityList = baseMapper.selectList(null);
            Map<String, List<Catalog2Vo>> catalogJson = new HashMap<>();
            // 先把一级菜单查出来
            List<CategoryEntity> topCategoryList = getByParentId(categoryEntityList, 0L);
            // 封装数据
            for (CategoryEntity category1Entity : topCategoryList) {
                // 对每一个一级菜单
                List<CategoryEntity> catalog2List =
                        getByParentId(categoryEntityList, category1Entity.getCatId());// 查一级菜单的二级菜单列表
                if (catalog2List != null && catalog2List.size() > 0) {
                    // 如果没有二级菜单，直接放都不用放，不会影响的
                    // 先新建一个List<Catalog2Vo>，一会肯定是要放东西的
                    List<Catalog2Vo> catalog2VoList = new ArrayList<>();
                    // 对每一个二级菜单做成Catalog2Vo，放到catalog2VoList中
                    for (CategoryEntity category2Entity : catalog2List) {
                        Catalog2Vo catalog2Vo = new Catalog2Vo();
                        catalog2Vo.setCatalog1Id(category1Entity.getCatId().toString());  // 一级菜单id
                        catalog2Vo.setId(category2Entity.getCatId().toString());  // 二级菜单id
                        catalog2Vo.setName(category2Entity.getName());  // 二级菜单name
                        List<Catalog3Vo> catalog3VoList = new ArrayList<>(Collections.emptyList());  // List<Catalog3Vo>可能为空可能不为空，但必须得放进去
                        List<CategoryEntity> catalog3List =
                                getByParentId(categoryEntityList, category2Entity.getCatId());// 查二级菜单的三级菜单列表
                        if (catalog3List != null && catalog3List.size() > 0) {
                            // 如果有三级菜单，就对每一个三级菜单做成Catalog3Vo，放到catalog3VoList中
                            for (CategoryEntity category3Entity : catalog3List) {
                                Catalog3Vo catalog3Vo = new Catalog3Vo();
                                catalog3Vo.setCatalog2Id(category2Entity.getCatId().toString());
                                catalog3Vo.setId(category3Entity.getCatId().toString());
                                catalog3Vo.setName(category3Entity.getName());
                                catalog3VoList.add(catalog3Vo);  // 放到catalog3VoList中
                            }
                        }
                        catalog2Vo.setCatalog3List(catalog3VoList);  // 三级菜单list
                        catalog2VoList.add(catalog2Vo);  // 放到catalog2VoList中
                    }
                    catalogJson.put(category1Entity.getCatId().toString(), catalog2VoList);
                }
            }
            // 将查到的数据放入缓存中，注意Map数据要先转化成JSON数据再放到redis中
            String jsonString = JSON.toJSONString(catalogJson);
            // 设置过期时间为5min
            ops.set("catalogJSON", jsonString, 5, TimeUnit.MINUTES);
            return catalogJson;

            ///////////////// 未经优化前的操作，用的是嵌套查询数据库  /////////
//        Map<String, List<Catalog2Vo>> catalogJson = new HashMap<>();
//        // 先把一级菜单查出来
//        List<CategoryEntity> topCategoryList = this.getTopCategorys();
//        // 封装数据
//        for (CategoryEntity category1Entity : topCategoryList) {
//            // 对每一个一级菜单
//            List<CategoryEntity> catalog2List =
//                    this.list(new QueryWrapper<CategoryEntity>().
//                            eq("parent_cid", category1Entity.getCatId()));// 查一级菜单的二级菜单列表
//            if (catalog2List != null && catalog2List.size() > 0) {
//                // 如果没有二级菜单，直接放都不用放，不会影响的
//                // 先新建一个List<Catalog2Vo>，一会肯定是要放东西的
//                List<Catalog2Vo> catalog2VoList = new ArrayList<>();
//                // 对每一个二级菜单做成Catalog2Vo，放到catalog2VoList中
//                for (CategoryEntity category2Entity : catalog2List) {
//                    Catalog2Vo catalog2Vo = new Catalog2Vo();
//                    catalog2Vo.setCatalog1Id(category1Entity.getCatId().toString());  // 一级菜单id
//                    catalog2Vo.setId(category2Entity.getCatId().toString());  // 二级菜单id
//                    catalog2Vo.setName(category2Entity.getName());  // 二级菜单name
//                    List<Catalog3Vo> catalog3VoList = new ArrayList<>(Collections.emptyList());  // List<Catalog3Vo>可能为空可能不为空，但必须得放进去
//                    List<CategoryEntity> catalog3List =
//                            this.list(new QueryWrapper<CategoryEntity>().
//                                    eq("parent_cid", category2Entity.getCatId()));// 查二级菜单的三级菜单列表
//                    if (catalog3List != null && catalog3List.size() > 0) {
//                        // 如果有三级菜单，就对每一个三级菜单做成Catalog3Vo，放到catalog3VoList中
//                        for (CategoryEntity category3Entity : catalog3List) {
//                            Catalog3Vo catalog3Vo = new Catalog3Vo();
//                            catalog3Vo.setCatalog2Id(category2Entity.getCatId().toString());
//                            catalog3Vo.setId(category3Entity.getCatId().toString());
//                            catalog3Vo.setName(category3Entity.getName());
//                            catalog3VoList.add(catalog3Vo);  // 放到catalog3VoList中
//                        }
//                    }
//                    catalog2Vo.setCatalog3List(catalog3VoList);  // 三级菜单list
//                    catalog2VoList.add(catalog2Vo);  // 放到catalog2VoList中
//                }
//                catalogJson.put(category1Entity.getCatId().toString(), catalog2VoList);
//            }
//        }
//        return catalogJson;
        }
    }

    /**
     * thymeleaf专用
     * 从数据库中返回所有的二级分类菜单和三级分类菜单，并封装成thymeleaf可以使用的格式
     * 只有当redis中不存在所需要的数据时才会调用这个方法
     * <p>
     * 使用最基本的redis分布式锁
     *
     * @return
     */
    public Map<String, List<Catalog2Vo>> getCatalogJsonByDBWithRedisLock() {
        // 抢占分布式锁
        String uUID = UUID.randomUUID().toString();
        ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
        Boolean lock = ops.setIfAbsent("lock", uUID, 30, TimeUnit.SECONDS);
        if (lock) {
            // 当前线程抢占到分布式锁
            // 才允许去数据库中查询数据
            Map<String, List<Catalog2Vo>> dataFromDbExtract = new HashMap<>();
            // 用一个try块来包裹数据查询工作，防止在执行过程中出现不必要的错误
            try {
                dataFromDbExtract = getDataFromDbExtract();
            } finally {
                // 无论数据查询是否成功，都会执行删除锁的操作。
                // 只有锁值和自己UUID匹配，才允许删，否则不准删锁
                String luaScript = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then return redis.call(\"del\",KEYS[1]) else return 0 end";
                // 删除锁，返回值并没有什么用，只是这里演示一下，可以获取返回值
                stringRedisTemplate.execute(new DefaultRedisScript<>(luaScript, Long.class),
                        Arrays.asList("lock"), uUID);
            }
            return dataFromDbExtract;
        } else {
            // 没有抢占到分布式锁
            // 等待100ms后，重试是否可以获取到分布式锁
            try {
                Thread.sleep(100);
            } catch (Exception e) {
                System.out.println(e.getMessage());
            }

            // 直接调用自身就相当于重新获取分布式锁
            return getCatalogJsonByDBWithRedisLock();
        }
    }


    /**
     * thymeleaf专用
     * 从数据库中返回所有的二级分类菜单和三级分类菜单，并封装成thymeleaf可以使用的格式
     * 只有当redis中不存在所需要的数据时才会调用这个方法
     * <p>
     * 使用最基本的Redisson分布式锁
     *
     * @return
     */
    public Map<String, List<Catalog2Vo>> getCatalogJsonByDBWithRedissonLock() {
        // 获取一个读写，锁的名字尽量取和业务相关联的
        RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("product-CatalogJson-Lock");
        // 直接对获取到的读写锁上一个读锁，当然这个读锁在迸发读时是没有用的
        RLock rLock = readWriteLock.readLock();
        rLock.lock();
        // 由于Redisson锁本身阻塞且自旋，所以获取到了才会执行下面的方法
        Map<String, List<Catalog2Vo>> dataFromDbExtract = new HashMap<>();
        // 用一个try块来包裹数据查询工作，防止在执行过程中出现不必要的错误
        try {
            dataFromDbExtract = getDataFromDbExtract();
        } finally {
            rLock.unlock();
        }
        return dataFromDbExtract;
    }


    /**
     * 用于优化上面的那个方法，把所有的查询改成对已经查出来的所有的菜单进行筛选
     *
     * @param categoryEntityList 已经查出来的所有菜单集合
     * @param parent_cid         用于筛选的父菜单id
     * @return 筛选结果
     */
    private List<CategoryEntity> getByParentId(List<CategoryEntity> categoryEntityList, Long parent_cid) {
        List<CategoryEntity> collect = categoryEntityList.stream().filter(categoryEntity -> {
            return categoryEntity.getParentCid().equals(parent_cid);
        }).collect(Collectors.toList());
        return collect;
    }

    /**
     * 没啥用只是一个抽取的方法，用于简化getCatalogJsonByDBWithRedisLock()方法
     *
     * @return
     */
    private Map<String, List<Catalog2Vo>> getDataFromDbExtract() {
        String catalogJOSN = stringRedisTemplate.opsForValue().get("catalogJOSN");
        if (!StringUtils.isEmpty(catalogJOSN)) {
            Map<String, List<Catalog2Vo>> catalogJsonByRedis = JSON.parseObject(catalogJOSN,
                    new TypeReference<Map<String, List<Catalog2Vo>>>() {
                    });
            return catalogJsonByRedis;
        }
        List<CategoryEntity> categoryEntityList = baseMapper.selectList(null);
        Map<String, List<Catalog2Vo>> catalogJson = new HashMap<>();
        List<CategoryEntity> topCategoryList = getByParentId(categoryEntityList, 0L);
        for (CategoryEntity category1Entity : topCategoryList) {
            List<CategoryEntity> catalog2List =
                    getByParentId(categoryEntityList, category1Entity.getCatId());
            if (catalog2List != null && catalog2List.size() > 0) {
                List<Catalog2Vo> catalog2VoList = new ArrayList<>();
                for (CategoryEntity category2Entity : catalog2List) {
                    Catalog2Vo catalog2Vo = new Catalog2Vo();
                    catalog2Vo.setCatalog1Id(category1Entity.getCatId().toString());
                    catalog2Vo.setId(category2Entity.getCatId().toString());
                    catalog2Vo.setName(category2Entity.getName());
                    List<Catalog3Vo> catalog3VoList = new ArrayList<>(Collections.emptyList());
                    List<CategoryEntity> catalog3List =
                            getByParentId(categoryEntityList, category2Entity.getCatId());
                    if (catalog3List != null && catalog3List.size() > 0) {
                        for (CategoryEntity category3Entity : catalog3List) {
                            Catalog3Vo catalog3Vo = new Catalog3Vo();
                            catalog3Vo.setCatalog2Id(category2Entity.getCatId().toString());
                            catalog3Vo.setId(category3Entity.getCatId().toString());
                            catalog3Vo.setName(category3Entity.getName());
                            catalog3VoList.add(catalog3Vo);
                        }
                    }
                    catalog2Vo.setCatalog3List(catalog3VoList);
                    catalog2VoList.add(catalog2Vo);
                }
                catalogJson.put(category1Entity.getCatId().toString(), catalog2VoList);
            }
        }
        String jsonString = JSON.toJSONString(catalogJson);
        stringRedisTemplate.opsForValue().set("catalogJSON", jsonString, 5, TimeUnit.MINUTES);
        return catalogJson;
    }

}